import { Ionicons } from '@expo/vector-icons';
import { useLocalSearchParams, useRouter } from 'expo-router';
import { useCallback, useMemo, useRef, useState } from 'react';
import {
ActivityIndicator,
KeyboardAvoidingView,
Platform,
Pressable,
ScrollView,
StyleSheet,
Text,
TextInput,
View,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useConversation } from '../../src/hooks';
import { colors } from '../../src/theme';
import type { ChatCard, ChatMessage } from '../../src/types';
/* ===================== 子组件:顶部导航 ===================== */
function Header({
contactName,
companyName,
positionName,
avatar,
online,
onBack,
onMore,
}: {
contactName: string;
companyName: string;
positionName: string;
avatar: string;
online: boolean;
onBack: () => void;
onMore: () => void;
}) {
return (
{avatar}
{contactName}
{online && }
{companyName} · {positionName}
);
}
/* ===================== 子组件:双向匹配横幅 ===================== */
function MatchBanner({ score, contactName }: { score: number; contactName: string }) {
return (
恭喜!您与{contactName}双向匹配成功
匹配度 {score}% · 立即沟通
);
}
/* ===================== 子组件:消息气泡 ===================== */
function Avatar({ text }: { text: string }) {
return (
{text}
);
}
function TextBubble({
message,
isUser,
}: {
message: ChatMessage;
isUser: boolean;
}) {
return (
{message.content}
);
}
function SystemMessage({ content }: { content: string }) {
return (
{content}
);
}
/* ===================== 子组件:富消息卡片 ===================== */
function InterviewCard({ card }: { card: ChatCard }) {
return (
{card.title}
{card.items?.map((item, idx) => (
{idx + 1}
{item}
))}
console.log('[Chat] 查看全部预测题')}>
{card.actionText}
);
}
function SalaryCard({ card }: { card: ChatCard }) {
return (
{card.title}
{card.subtitle && (
{card.subtitle}
)}
console.log('[Chat] 查看薪资报告')}>
{card.actionText}
);
}
function OfferCard({ card }: { card: ChatCard }) {
const router = useRouter();
return (
{card.title}
{card.subtitle && {card.subtitle}}
router.push('/jobs/offer-compare')}>
{card.actionText}
);
}
function MessageCard({ card }: { card: ChatCard }) {
switch (card.type) {
case 'interview':
return ;
case 'salary':
return ;
case 'offer':
return ;
default:
return null;
}
}
/* ===================== 子组件:单条消息行 ===================== */
function MessageRow({
message,
avatar,
}: {
message: ChatMessage;
avatar: string;
}) {
const isUser = message.role === 'user';
const isSystem = message.role === 'system';
if (isSystem) {
return ;
}
return (
{!isUser && }
{message.content ? (
) : null}
{message.card ? : null}
);
}
/* ===================== 子组件:时间戳 ===================== */
function Timestamp({ time }: { time: string }) {
return (
{time}
);
}
/* ===================== 子组件:底部输入框 ===================== */
function InputBar({
value,
onChange,
onSend,
sending,
}: {
value: string;
onChange: (text: string) => void;
onSend: () => void;
sending: boolean;
}) {
return (
console.log('[Chat] 表情')}>
console.log('[Chat] 图片')}>
console.log('[Chat] 文件')}>
console.log('[Chat] 更多')}>
);
}
/* ===================== 页面 ===================== */
export default function ChatDetailScreen() {
const router = useRouter();
const { id } = useLocalSearchParams<{ id: string }>();
const { detail, loading, error, refresh, sendMessage, sending } = useConversation(id ?? '');
const [input, setInput] = useState('');
const scrollRef = useRef(null);
const messages = useMemo(() => detail?.messages ?? [], [detail]);
const handleSend = useCallback(async () => {
const text = input.trim();
if (!text || sending) return;
setInput('');
await sendMessage(text);
// 滚动到底部
setTimeout(() => scrollRef.current?.scrollToEnd({ animated: true }), 100);
}, [input, sending, sendMessage]);
if (loading) {
return (
);
}
if (error || !detail) {
return (
{error ? '加载失败' : '会话不存在'}
重试
);
}
return (
router.back()}
onMore={() => console.log('[ChatDetail] 更多操作')}
/>
{detail.matchInfo && (
)}
scrollRef.current?.scrollToEnd({ animated: false })}
>
{messages.map((message, index) => {
const showTime =
index === 0 ||
messages[index - 1].timestamp !== message.timestamp ||
message.role !== messages[index - 1].role;
return (
{showTime && message.timestamp ? (
) : null}
);
})}
);
}
const styles = StyleSheet.create({
safeArea: {
flex: 1,
backgroundColor: colors.pageBg,
},
flex: {
flex: 1,
},
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 8,
height: 52,
backgroundColor: colors.white,
borderBottomWidth: 1,
borderBottomColor: colors.borderLight,
},
headerBtn: {
width: 40,
height: 40,
alignItems: 'center',
justifyContent: 'center',
},
headerCenter: {
flexDirection: 'row',
alignItems: 'center',
flex: 1,
justifyContent: 'center',
},
headerInfo: {
marginLeft: 10,
alignItems: 'center',
},
headerNameRow: {
flexDirection: 'row',
alignItems: 'center',
},
headerName: {
fontSize: 16,
fontWeight: '700',
color: colors.textPrimary,
},
onlineDot: {
width: 8,
height: 8,
borderRadius: 4,
backgroundColor: colors.success,
marginLeft: 6,
},
headerSub: {
marginTop: 2,
fontSize: 12,
color: colors.textSecondary,
},
avatar: {
width: 36,
height: 36,
borderRadius: 18,
alignItems: 'center',
justifyContent: 'center',
},
avatarText: {
fontSize: 14,
fontWeight: '700',
color: colors.white,
},
banner: {
flexDirection: 'row',
alignItems: 'center',
marginHorizontal: 8,
marginTop: 8,
padding: 10,
borderRadius: 8,
backgroundColor: colors.successLight,
},
bannerIconWrap: {
width: 34,
height: 34,
borderRadius: 17,
backgroundColor: colors.white,
alignItems: 'center',
justifyContent: 'center',
marginRight: 10,
},
bannerBody: {
flex: 1,
},
bannerTitle: {
fontSize: 14,
fontWeight: '700',
color: colors.successText,
},
bannerMeta: {
marginTop: 2,
fontSize: 12,
color: colors.success,
},
scroll: {
flex: 1,
},
scrollContent: {
padding: 8,
paddingBottom: 8,
},
systemWrap: {
alignItems: 'center',
marginVertical: 10,
},
systemText: {
fontSize: 12,
color: colors.textHint,
},
timeWrap: {
alignItems: 'center',
marginVertical: 6,
},
timeText: {
fontSize: 11,
color: colors.textHint,
},
messageRow: {
flexDirection: 'row',
marginBottom: 10,
maxWidth: '82%',
},
messageRowLeft: {
alignSelf: 'flex-start',
},
messageRowRight: {
alignSelf: 'flex-end',
flexDirection: 'row-reverse',
},
messageBody: {
marginLeft: 8,
marginRight: 8,
},
bubble: {
paddingHorizontal: 12,
paddingVertical: 9,
borderRadius: 12,
maxWidth: '100%',
},
bubbleHr: {
backgroundColor: colors.white,
borderBottomLeftRadius: 4,
},
bubbleUser: {
backgroundColor: colors.primary,
borderBottomRightRadius: 4,
},
bubbleText: {
fontSize: 14,
lineHeight: 20,
},
bubbleTextHr: {
color: colors.textPrimary,
},
bubbleTextUser: {
color: colors.white,
},
cardBase: {
width: 260,
backgroundColor: colors.white,
borderRadius: 10,
padding: 12,
marginTop: 6,
},
cardHeader: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 8,
},
cardTitle: {
marginLeft: 6,
fontSize: 14,
fontWeight: '700',
color: colors.textPrimary,
},
cardSalary: {
backgroundColor: colors.primaryLight,
},
cardList: {
marginBottom: 8,
},
cardListRow: {
flexDirection: 'row',
alignItems: 'flex-start',
marginBottom: 6,
},
cardBullet: {
width: 16,
height: 16,
borderRadius: 8,
backgroundColor: colors.successLight,
alignItems: 'center',
justifyContent: 'center',
marginRight: 6,
marginTop: 2,
},
cardBulletText: {
fontSize: 10,
fontWeight: '700',
color: colors.success,
},
cardItemText: {
flex: 1,
fontSize: 13,
color: colors.textPrimary,
},
cardSubtitle: {
fontSize: 12,
lineHeight: 18,
color: colors.textSecondary,
marginBottom: 10,
},
cardAction: {
flexDirection: 'row',
alignItems: 'center',
paddingTop: 8,
borderTopWidth: 1,
borderTopColor: colors.borderLight,
},
cardActionText: {
flex: 1,
fontSize: 12,
color: colors.primary,
fontWeight: '600',
},
cardOffer: {
width: 260,
backgroundColor: colors.actionLight,
borderRadius: 10,
padding: 12,
marginTop: 6,
},
cardOfferHeader: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 10,
},
cardOfferTitleWrap: {
flex: 1,
marginLeft: 10,
},
cardOfferTitle: {
fontSize: 14,
fontWeight: '700',
color: colors.actionText,
},
cardOfferSubtitle: {
marginTop: 2,
fontSize: 12,
color: colors.textSecondary,
},
cardOfferBtn: {
flexDirection: 'row',
alignItems: 'center',
paddingTop: 8,
borderTopWidth: 1,
borderTopColor: colors.actionBorder,
},
cardOfferBtnText: {
flex: 1,
fontSize: 12,
color: colors.actionText,
fontWeight: '600',
},
inputBar: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 6,
paddingVertical: 8,
backgroundColor: colors.white,
borderTopWidth: 1,
borderTopColor: colors.borderLight,
},
inputLeftActions: {
flexDirection: 'row',
alignItems: 'center',
},
inputIconBtn: {
width: 34,
height: 34,
alignItems: 'center',
justifyContent: 'center',
},
inputWrap: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
marginHorizontal: 4,
paddingHorizontal: 10,
paddingVertical: 4,
borderRadius: 18,
backgroundColor: colors.pageBg,
},
input: {
flex: 1,
maxHeight: 80,
minHeight: 34,
fontSize: 14,
color: colors.textPrimary,
paddingRight: 8,
},
plusBtn: {
width: 28,
height: 28,
alignItems: 'center',
justifyContent: 'center',
},
sendBtn: {
width: 36,
height: 36,
borderRadius: 18,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: colors.primaryBtn,
},
sendBtnDisabled: {
opacity: 0.5,
},
stateBox: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
stateText: {
marginTop: 12,
fontSize: 14,
color: colors.textSecondary,
},
retryBtn: {
marginTop: 16,
paddingHorizontal: 24,
paddingVertical: 8,
borderRadius: 18,
backgroundColor: colors.primaryBtn,
},
retryText: {
fontSize: 13,
fontWeight: '600',
color: colors.white,
},
});